package netsi;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.AlgorithmParameterGenerator;
import java.security.AlgorithmParameters;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidParameterSpecException;

import javax.crypto.spec.DHParameterSpec;
// Import log4j classes.
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.BasicConfigurator;

/**
 * 
 * Sender accepts incoming connection on @param port and creates a new thread
 * for each connection. The Worker-Thread uses Diffie-Hellman to establish a
 * session key over an insecure communication channel. The key is then used
 * to encrypt subsequent communications using a symmetric key cipher e.g. we can
 * use a symmetric cipher for the exchange of the position in the otp key-text :
 * {@link #KEYTEXT} and the length of the key. It is assumed that only the two parties
 * have access to the {@link #KEYTEXT} text file. So for a true encrypted communication
 * the {@link #KEYTEXT} file has to be distributed by other means e.g. via mail etc.
 * 
 * @author Bao To & Chris Yereaztian
 * 
 */
public class Sender extends Thread{
	
	public static final Logger logger = Logger.getLogger("Sender");
	public static final int LISTENPORT = 11111;
	public static final String KEYTEXT = "res/crsto10.txt";
	
	private static final String MESSAGEPATH = "res/input.txt";
	
	//just for demonstration purposes will always take a key starting from character 100
	//to make one time pad secure, no key should be used twice, means that the starting position
	// in the text should be different every time we send a message.
	private final long posInText = 100;
	
	private ServerSocket server;
	
	public Sender(){
		this(Sender.LISTENPORT);
	}
	
	/**
	 * 
	 * @param port, port on which the server should listen and await connection from a client
	 */
	public Sender(int port) {
		try {
			server = new ServerSocket(port);
			logger.info("Server listening on port: " + port);
			this.start();
		} catch (IOException e) {
			logger.fatal("Couldn't bind port to socket. Port is already in use."
					+ "Check if you are already running a server. Exiting...");
			e.printStackTrace();
			System.exit(-1);
		}
	}
	
	/**
	 * The Sender awaits a connection on the @param port and creates a new Sender-Worker
	 * Thread for each client which handles the establishment of the DH session key and the
	 * transfer of the Position of the OTP-Key in the {@link #KEYTEXT} file as well as the length of
	 * the key. Given the starting position and the length of the otp key, Bob can read the key
	 * from the same {@link #KEYTEXT} file, that needs to be distributed by other secure means.
	 */
	@Override
	public void run(){
		SenderWorker w;
		Socket cl;
		while (true) {
			try {
				logger.info("Waiting for connection from client ...");
				cl = server.accept();
				logger.info("Accepted a connection from: "
						+ cl.getInetAddress());
				w = new SenderWorker(cl);
				Thread t = new Thread(w);
				t.start();

			} catch (IOException ioe) {
				logger.debug("Client: disconnected while waiting for server to accept the connection");
			}
		}
	}
	
	/**
	 * Reads the OTP Key from the keytext file
	 * @param keylength, the length of the key. In OTP The length of the key is equal
	 * to the length of the message. This inherently means that one message cannot be
	 * longer than the total length of the keytext file.
	 * @return A String from the given keytext file
	 */
	public String readKeyFromFile(int keylength){
		FileReader fin;
		char[] key = new char[keylength];
		try {
			fin = new FileReader(KEYTEXT);
			fin.skip(posInText);
			BufferedReader reader = new BufferedReader(fin);
			int count = reader.read(key);
			if (count < keylength || count == -1){
				logger.error("Error Reading Key: Reached End of Stream - consider repositiong");
			}
			logger.debug("Key: " + new String(key));
		} catch (IOException e) {
			logger.fatal("Could not read OTP key from keytext file. R: " +e.getMessage());
		}
		return new String(key);
	}
	
	/**
	 * Executes simple encryption procedure by XOR-ing every character of the given cipherkey with
	 * the given message. This requires that the cipherkey and the message are of the same length
	 * otherwise the message will partially be not encrypted and sent in plaintext over the insecure
	 * communication channel.
	 * @param cipherkey, the OTP key e.g. the string returned from readKeyFromFile()
	 * @param message, the server is trying to send to the client
	 * @return encrypted message
	 */
	public String encryptText(String cipherkey, String message){
		//check that the Strings are of equal length
		if (cipherkey.length() != message.length())
			logger.warn("Key and Message are not of equal length - message will partially be transmitted in plain text");
		//XORing the two strings at character level
		StringBuilder sb = new StringBuilder();
		for(int i=0; i<cipherkey.length() && i<message.length();i++)
		    sb.append((char)(cipherkey.charAt(i) ^ message.charAt(i)));
		String result = sb.toString();
		logger.debug("Encrypted text: " + result);
		return result;
	}
	
	/**
	 * 
	 * @param path, path to the .txt file which contains the message the server should send
	 * @return Message as a string which has been read from the given file
	 */
	private String readMessageFromFile(String path){
		StringBuilder contents = new StringBuilder();
		try {
			FileReader fin = new FileReader(MESSAGEPATH);
			BufferedReader input = new BufferedReader(fin);
			try {
				String line = null; // not declared within while loop
				/*
				 * readLine is a bit quirky : it returns the content of a line
				 * MINUS the newline. it returns null only for the END of the
				 * stream. it returns an empty String if two newlines appear in
				 * a row.
				 */
				while ((line = input.readLine()) != null) {
					contents.append(line);
					//contents.append(System.getProperty("line.separator"));
				}
			} finally {
				input.close();
			}
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		return contents.toString();
	}
	
	/**
	 * The Sender creates a SenderWorker Thread for each client
	 * SenderWorker handles the Key-Exchange as well as the cipher-text exchange
	 * before the first key exchange a session key is exchanged using Diffie-Hellman
	 * for more details about DH: http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
	 * the server generates the prime number and the generator
	 * @author bao & chris
	 *
	 */
	class SenderWorker extends Thread{
		//Alice's secret number, this is inherently insecure
		//should use java.security.KeyPairGenerator; to generate random secret
		//this is only for demonstration purposes
		private final int alicesecret = 3;
		//DH Parameters
		private AlgorithmParameterGenerator paramGen;
		private BigInteger p;
		private BigInteger g;
		//Socket
		private Socket client;
		private ObjectInputStream oin;
		private ObjectOutputStream oout;
		/* OTP Key, only known by Alice and Bob. In OTP, the actual Key
		 * is never transmitted over the insecure channel. Only the starting position
		 * of the OTP Key in the KEYTEXT file and the length of the key is transmitted
		 */
		private BigInteger sharedSecret;
		//Constant for the parameter generator - generates p,g,
		private static final String ALGORITHM = "DH";
		
		/**
		 * One SenderWorker handles the request from one client. It's responsible
		 * for the DH Session Key exchange as well as the exchange of the starting position of the 
		 * otp key and the actual transmission of the message.
		 * @param client, Socket from the main server thread which accepts connection
		 * from any client
		 */
		public SenderWorker(Socket client){
			this.client = client;
			try {
				oout = new ObjectOutputStream(this.client.getOutputStream());
				oout.flush();
				oin = new ObjectInputStream(this.client.getInputStream());

			} catch (IOException e) {
				logger.error("Client disconnected: "+e.getMessage());
			}
			this.start();
		}

		@Override
		public void run() {
			try{		
				//ParameterGenerator for Diffie Hellman: prime number p and 
				//g is primitive root mod p
				paramGen = AlgorithmParameterGenerator.getInstance(ALGORITHM);
				//Generating DH Parameters
				paramGen.init(512);
				AlgorithmParameters params =  paramGen.generateParameters();
				//retrieving the generated parameteres
				DHParameterSpec dhSpec = (DHParameterSpec) params.getParameterSpec(DHParameterSpec.class);
				p = dhSpec.getP();
				g = dhSpec.getG();
				logger.debug("Prime number: " + p);
				logger.debug("g as primitive root mod p: " + g);
			} catch (NoSuchAlgorithmException e) {
				logger.error("Couldn't get Instance of Algorithm: " + ALGORITHM);
			} catch (InvalidParameterSpecException e) {
				logger.error("Invalid Diffie Hellmann Parameters");
			}
			this.exchangeSessionKey();
			this.sendMessageFromFile(MESSAGEPATH);
		}
		/**
		 * Exchanges the DH Parameters with the client to establish
		 * a shared session key (sharedSecret) using the Diffie Hellman
		 * Key Exchangee, the shared secret is than used to transmit the starting
		 * position of the otp key in the crst10.txt
		 * 
		 */
		private void exchangeSessionKey() {
			//establish connection with client to exchange p and g
			try{
				//sending p and g
				oout.writeObject(p);
				oout.writeObject(g);
				logger.info("Sending prime: " + p);
				logger.info("Sending generator: " + g);
				//sending A = g^alicesecret mod p
				BigInteger ga = g.pow(alicesecret).mod(p);
				logger.info("Sending ga: "+ga);
				oout.writeObject(ga);
				oout.flush();
				//Waiting for Bobs B = g^bobsecret mod p
				Object gb = oin.readObject();
				BigInteger gbint = (BigInteger)gb;
				logger.info("Received Bob's g^b mod p : " + gbint);
				//Calculate shared secret gbit ^ alicesecret mod p;
				sharedSecret = gbint.pow(alicesecret).mod(p);
				logger.debug("Shared Secret: "+ sharedSecret);
				
			} catch(IOException io){
				logger.info("Broken Pipe: Client must have disconnected: " + io.getMessage());
			} catch (ClassNotFoundException e) {
				logger.debug("ClassNotFound Exception: something went wrong while" +
						"trying to read object from networkstream");
			} catch (NullPointerException ne){
				logger.debug("Probably couldn't write to SocketStream; " + ne.getMessage());
			}
		}
		
		/**
		 * Initiates the sending of the message. The Worker Thread
		 * encrypts the textposition of the key in the otp key text with the sharedSecret
		 * (Session Key from DH) and furthermore encrypts the message with the OTP
		 * key.
		 * @param path, path to file which contains the message
		 */
		public void sendMessageFromFile(String path) {
			String msg = Sender.this.readMessageFromFile(path);
			logger.info("Msg read from file: " + msg);
			String cipherkey = Sender.this.readKeyFromFile(msg.length());
			String msgencrypted = Sender.this.encryptText(cipherkey, msg);
			logger.info("Sending encrypted msg: "+ msgencrypted);
			try {
				//exchanging OTP Text Position
				//cheap encryption: XOR shared secret with position in text
				BigInteger encryptedPos = sharedSecret.xor(BigInteger.valueOf(posInText));
				logger.debug("Sending encrypted textpos: "+encryptedPos);
				BigInteger encryptedOTPKeyLength = sharedSecret.xor(BigInteger.valueOf(msg.length()));
				logger.debug("Sending encrypted OTPKeyLength: "+encryptedOTPKeyLength);
				oout.writeObject(encryptedPos);
				oout.writeObject(encryptedOTPKeyLength);
				oout.writeObject(msgencrypted);
				oout.flush();
				//close streams
				client.shutdownInput();
				client.shutdownOutput();
			} catch (IOException e) {
				logger.debug("Client must have disconnected during transmission" + e.getMessage());
			}		
			
		}
		
	}
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// Set up a simple logger configuration that logs on the console.
		BasicConfigurator.configure();
		//sets loglevel
		logger.setLevel(Level.ALL);
		try{
			Sender s = new Sender();

		}catch(Exception e){}
		
	
	}

}
